/*
Kakine
ActionScript Framework
Copyright © 2011, PUNY, LLC.

This file is part of KAKINE.

KAKINE is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

KAKINE is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with KAKINE.  If not, see <http://www.gnu.org/licenses/>.
*/ 
package com.puny.utils
{
	import com.puny.mvc.MvcEvent;
	import com.puny.mvc.helpers.DelayHelper;
	import com.puny.mvc.models.Model;
	import com.puny.xmlutils.XMLReader;
	
	import flash.events.Event;

	/**
	 * Petri Net Engine: It can't be useful as a state machine.
	 * A state char should be provided as XML file or string.
	 * <table>
	 * <tr><td><code>&lt;configure&gt;</code></td><td>a root element. No attributes are required.</td></tr>
	 * <tr><td><code>&lt;network&gt;</code></td><td>a group of places and transitions. a <code>configuraton</code> can have multiple network elements.</td></tr>
	 * <tr><td></td><td><code>focus</code> (optional) is an optional attribute. If its value is <code>on</code>, name of all places in this netowrk will be handled as a state. Otherwise, place names are ignored.</td></tr>
	 * <tr><td><code>&lt;place&gt;</code></td><td>place definition</td></tr>
	 * <tr><td><code></code></td><td><code>id</code> should be unique.</td></tr>
	 * <tr><td><code></code></td><td><code>name</code> (optional)</td></tr>
	 * <tr><td><code></code></td><td><code>from</code> (optional)</td></tr>
	 * <tr><td><code>&lt;transition&gt;</code></td><td>transition definition</td></tr>
	 * <tr><td><code></code></td><td><code>id</code> should be unique.</td></tr>
	 * <tr><td><code></code></td><td><code>name</code> (optional)</td></tr>
	 * <tr><td><code></code></td><td><code>from</code> (optional)</td></tr>
	 * </table>
	 * @example sample configuration
	 * <listing>
	 * &lt;configure&gt;
	 *   &lt;network focus='on'&gt;
	 *     &lt;place id='place0' token='1'/&gt;
	 *     &lt;transition id='tran0' name='my transition' from='place0'/&gt;
	 *     &lt;place id='place1' name='my place' from='tran0'/&gt;
	 *   &lt;/network&gt;
	 *   &lt;network focus='off'&gt;
	 *     &lt;place id='place2' token='1'/&gt;
	 *   &lt;/network&gt;
	 *   &lt;network&gt;
	 *     &lt;place id='place3' token='1'/&gt;
	 *   &lt;/network&gt;
	 * &lt;/configure&gt;
	 * </listing>
	 *      @see #transition()
	 */
	public class PetriNet extends EnhancedEventDispatcher
	{
		private static const START:String = "start";
		private static const TOKEN:String = "token";
		private static const FROM:String = "from";
		private static const TRANSITIONS:String = "transitions";
		private static const PLACES:String = "places";
		private static const NEXT_MODELS:String = "nextModels";
		private static const RATE:String = "rate";
		private var _places:Array;
		private var _transitions:Array;
		private var _life:int;
		private var _filename:String;
		private var _currentState:String;
		private var _configPlace:Model;
		private var _configTransition:Model;
		private var _configArc:Model;
		
		/**
		 * Constructor
		 * @param configuration XML string or XML file name.
		 * 
		 */
		public function PetriNet(config:Object) {
			if (config is String) {
				_filename = config as String;
				var reader:XMLReader = new XMLReader();
				reader.addEventListener(XMLReader.COMPLETED, completedHandler);
				reader.setURL(_filename);
			} else if (config is XML) {
//				DelayHelper.setup(500,null,function ():void {
					configuration(config as XML);
					initializeModels(config.network)
//				});
			} else {
				throw new ArgumentError("String and XML instances are supported");
			}
		}
		
		public function get configPlace():Model {
			return _configPlace;
		}
		
		public function get configTransition():Model {
			return _configTransition;
		}
		
		public function get configArc():Model {
			return _configArc;
		}
		
		public function close():void {
			var model:Model;
			for each(model in _places) {
				model.removeEventListener("token", updatedHandler);
				model.listeners = null;
			}
			for each(model in _transitions) {
				model.removeEventListener("token", updatedHandler);
				model.listeners = null;
			}
		}
		private function completedHandler(event:Event):void {
			event.currentTarget.removeEventListener(XMLReader.COMPLETED, completedHandler);
			configuration(XMLReader.xmlDoc(_filename));
			initializeModels(XMLReader.xmlDoc(_filename).network);
		}
		
		private function configuration(config:XML):void {
			_configPlace = parseConfig(config.place[0]);
			_configTransition = parseConfig(config.transition[0]);
			_configArc = parseConfig(config.arc[0]);
			_configPlace.token = parseConfig(config.place[0].token[0]);
		}
	
		private function initializeModels(networks:XMLList):void {
			_places = new Array();
			_transitions = new Array();
			var local:Array;
			var focus:Boolean;
			var place:Model;
			
			for each(var network:XML in networks) {
				local = new Array();
				local = setupModels(network.elements("place"));
				focus = network.hasOwnProperty("@focus") && network.@focus=="on";
				for each(place in local) {
					place.focus = focus;
				}
				_places = _places.concat(local);
				_transitions = _transitions.concat(setupModels(network.elements("transition")));
			}
			_life = _transitions.length;
			makeNetwork(_places,_transitions);
			makeNetwork(_transitions,_places);
			currentCombination();
			dispatchEvent(new MvcEvent(MvcEvent.READY));
		}
		
		private function parseConfig(item:XML):Model {
			var model:Model = new Model();
			for each(var attribute:XML in item.attributes()) {
				model[attribute.name()] = attribute;
			}
			return model;
		}
		
		private function getName(model:Model):String {
			return model.hasOwnProperty("name") ? model.name : model.id;
		}
		
		private function setupModels(xmlList:XMLList):Array {
			var model:Model;
			var array:Array = new Array();
			for each(var item:XML in xmlList) {
				model = new Model();//EntityModels.getInstance(item.@id);
				model.x = 0;
				model.y = 0;
				model.token = 0;
				model.rate = 10;
				model.prevModels = new Array();
				model.nextModels = new Array();
				model.count = 0;
				model.listeners = new Array();
//				if (!model.hasOwnProperty("count")) {
//					model.count = 0;
//				}
				for each(var attr:XML in item.attributes()) {
					model[attr.name()] = attr;
				}
				model.initialToken = model.token;
				array.push(model);
			}
			return array;
		}

		public function findModel(models:Array,id:String):Model {
			for each(var model:Model in models) {
				if (model.id == id) {
					return model;
				}
			}
			return null;
		}
		
		private function makeNetwork(parents:Array,children:Array):void {
			var ids:String;
			var item:Model;
			for each(var model:Model in children) {
				if (model.hasOwnProperty(FROM)) {
					ids = model.from;
					if (ids.length != 0) {
						for each(var id:String in ids.split(",")) {
							item = findModel(parents, id);
							if (item != null) {
								model.prevModels.push(item);
								item.nextModels.push(model);
							} else {
								throw new ReferenceError(id + " on " + model.id + " can't be found");
							}
						}
					}
				}
			}	
		}
		
		private function checkPlaces(transition:Model):String {
			var place:Model;
			
			//Push a token from arcs.
			if (transition.token > 0) {
//				if (transition.hasOwnProperty(NEXT_MODELS)) {
					for each(place in transition.nextModels) {
//						trace("Added to",place.id);
						place.beginUpdate();
						place.token++;
						place.count++;
					}
					transition.nextModels.forEach(Model.commit);
//					trace("Took from",transition.id);
					if( transition.nextModels.length > 0 ) {
						transition.token--;
					}
//				}
			}
			return currentCombination();
		}
		
		private function canFireByModel(transition:Model):Boolean {
			var all:Boolean = true;
			var place:Model;
			for each(place in transition.prevModels) {
//				trace(place.id, place.token);
				all = (all && place.token > 0);
			}
			return all;
		}
		
		/**
		 * Returns name list of current places. If a place have no name attibutes, id will be its name. 
		 * @return name list of places which have a token. Colon separated string. 
		 * 
		 */
		protected function currentCombination():String {
			var place:Model;
			var value:String = "";
			for each(place in _places) {
				if (place.token > 0 && place.focus) {
					if (value.length > 0) {
						value += ":";
					}
					value += getName(place);
				}
			}
			_currentState = value;
			return value;
		}

		/**
		 * Returns name list of current places. If a place have no name attibutes, id will be its name. 
		 * @return name list of places which have a token. Colon separated string. 
		 */
		public function get currentState():String {
			return _currentState;
		}
		
		/**
		 * If the given transtion has a token, it pushes the token to the next places.
		 * @param value transition id
		 * @return If the given token is fired, name list of places which have a token. Otherwise, zero length string.
		 */
		public function pushTokenToPlace(value:String):String {
			var transition:Model = findModel(_transitions,value);
			if (transition != null && transition.token > 0) {
				return checkPlaces(transition);
			}
			return "";
		}
		
		public function addPlaceListeners(id:String,func:Function):void {
			addStateListeners(findModel(_places,id), func);			
		}
		
		public function removePlaceListeners(id:String,func:Function):void {
			removeStateListeners(findModel(_places,id), func);
		}
		
		public function addTransitionListeners(id:String,func:Function):void {
			addStateListeners(findModel(_transitions,id), func);			
		}
		
		public function removeTransitionListeners(id:String,func:Function):void {
			removeStateListeners(findModel(_transitions,id), func);
		}

		private function addStateListeners(model:Model,func:Function):void {
			if (model.listeners.length == 0) {
				model.addEventListener("token", updatedHandler);
			}
			model.listeners.push(func);
		}
		
		private function removeStateListeners(model:Model,func:Function):void {
			var index:int = model.listeners.indexOf(func);
			if (index >= 0) {
				model.listeners.splice(func);
			}
			if (model.listeners.length == 0) {
				model.removeEventListener("token", updatedHandler);
			}
		}
		
		private function updatedHandler(event:Event):void {
			var model:* = event.currentTarget;
			var func:Function;
			if (model.token > 0) {
				for each(func in model.listeners) {
					func(event);
				}
			}
		}
		
		/**
		 * Array of transitions
		 * @return transion array
		 * 
		 */
		public function get transitions():Array {
			return _transitions;
		}
		
		/**
		 * Array of places
		 * @return place array 
		 * 
		 */
		public function get places():Array {
			return _places;
		}
		
		public function place(value:String):Model {
			return findModel(_places, value);
		}
		
		/**
		 * 
		 * @param value transition id
		 * @return <code>TRUE</code> if the given transtion can be fired. Otherwise, <code>false</code>
		 * 
		 */
		public function canFire(value:String):Boolean {
			var transition:Model = findModel(_transitions,value);
			if (transition != null) {
				return canFireByModel(transition);
			}
			return false;
		}
		
		/**
		 * If the given transition can be fired, it takes tokens from places.
		 * @param value transition id
		 * @return transition instance
		 * 
		 */
		public function takeTokenFromPlace(value:String):Model {
			var transition:Model = findModel(_transitions,value);
			var place:Model;
			if (transition != null) {
				if (canFireByModel(transition)) {
					for each(place in transition.prevModels) {
						place.beginUpdate();
						place.token--;
//						trace("Took from", place.id);
					}
					transition.prevModels.forEach(Model.commit);

					transition.token++;
//					trace("Added to", transition.id);
					_currentState = "";
					transition.count++;
					return transition;
				}				
//			} else {
//				throw new SyntaxError(transition+" can't be found.");
			}
			return null;
		}
		
		/**
		 * Takes token from palces and push the token to the next places if the given transition can be fired. 
		 * @param value transition id
		 * @return name list of places which have tokens if the transition is fired. Otherwise, zero length string.
		 * 
		 */
		public function transit(value:String):String {
			var transition:Model = takeTokenFromPlace(value);
			if (transition==null) {
				return "";
			}
			return checkPlaces(transition);
		}
		
		/**
		 * Initialize Petri Net.
		 * 
		 */
		public function initialize():void {
			var model:Model;
			for each(model in _transitions) {
				model.token = model.initialToken;
				model.count = 0;
			}
			for each(model in _places) {
				model.token = model.initialToken;
				model.count = 0;
			}
			currentCombination();
		}
		
	}
}